package org.hybridlabs.source.formatter.m2;

/**
 * Copyright 2008 hybrid labs 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0 
 *   
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hybridlabs.file.FileHelper;
import org.hybridlabs.source.beautifier.CharacterSequence;
import org.hybridlabs.source.formatter.JavaImportBeautifier;

/**
 * A utility to drive the code beautifier. Note the system properties that point
 * to the various configuration parameters used by the utility.
 * 
 * @author Jack Archer, Bob Fields
 * @version 1.0, Feb. 2007
 */
public class FormatManager {
    private String conventionFilePath = System
            .getProperty("convention.file.path");

    private String conventionFileName = System.getProperty(
            "convention.file.name", "default-convention.xml");

    private final String slash = System.getProperty("file.separator");

    private static final Log LOG = LogFactory.getLog(FormatManager.class);

    /**
     * Run as ant task from BuildCT.xml. Formats imports and Jalopy. If args[1]
     * is empty, source input directory files will be overwritten. System
     * properties control behavior: convention.file.path is the path to the
     * Jalopy convention file (normally org/hybridlabs under ${m2repo}
     * convention.file.name is the name if the Jalopy convention file
     * updateImports true = convert fully qualified classnames to imports
     * updateJalopy true = run Jalopy formatting against output files
     * 
     * @param args
     *            1 - root directory of the source to be formatted 2- target
     *            directory to write formatted source to
     */
    public static void main(String[] args) {
        try {
            // Format imports and Jalopy separately because file will be rolled
            // back if
            // Jalopy parsing fails.
            boolean updateImports = Boolean.parseBoolean(System.getProperty(
                    "updateImports", "true"));

            boolean updateJalopy = Boolean.parseBoolean(System.getProperty(
                    "updateJalopy", "true"));

            LOG.info("Formatting " + args[0] + " updateImports="
                    + updateImports + " updateJalopy=" + updateJalopy);
            if (args.length > 1) {
                if (updateImports) {
                    FormatManager.formatImportsAllFiles(args[0], args[1]);
                }
                if (updateJalopy) {
                    FormatManager.formatJalopyAllFiles(args[0], args[1]);
                }
            } else {
                if (updateImports) {
                    FormatManager.formatImportsAllFiles(args[0]);
                }
                if (updateJalopy) {
                    FormatManager.formatJalopyAllFiles(args[0]);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Formats all Java files discovered in the supplied directory and
     * subdirectories.
     * 
     * @param sourceRootDirectory
     *            Directory for Java files to format and replace
     * @throws FileNotFoundException
     *             If required files or directories do not exist
     * @throws IOException
     *             If the creation of a new file or directory fails
     */
    public static void formatJalopyAllFiles(String sourceRootDirectory)
            throws FileNotFoundException, IOException {
        formatAllFiles(sourceRootDirectory, sourceRootDirectory, false, true);
    }

    /**
     * Formats all Java files discovered in the supplied directory and
     * subdirectories.
     * 
     * @param sourceRootDirectory
     *            Directory for Java files to format and replace
     * @param targetRootDirectory
     *            Target Java source output directory (if null, use source
     *            directory)
     * @throws FileNotFoundException
     *             If required files or directories do not exist
     * @throws IOException
     *             If the creation of a new file or directory fails
     */
    public static void formatImportsAllFiles(String sourceRootDirectory,
            String targetRootDirectory) throws FileNotFoundException,
            IOException {
        formatAllFiles(sourceRootDirectory, targetRootDirectory, true, false);
    }

    /**
     * Formats all Java files discovered in the supplied directory and
     * subdirectories.
     * 
     * @param sourceRootDirectory
     *            Directory for Java files to format and replace
     * @param targetRootDirectory
     *            Target Java source output directory (if null, use source
     *            directory)
     * @throws FileNotFoundException
     *             If required files or directories do not exist
     * @throws IOException
     *             If the creation of a new file or directory fails
     */
    public static void formatJalopyAllFiles(String sourceRootDirectory,
            String targetRootDirectory) throws FileNotFoundException,
            IOException {
        formatAllFiles(sourceRootDirectory, targetRootDirectory, false, true);
    }

    /**
     * Formats all Java files discovered in the supplied directory and
     * subdirectories.
     * 
     * @param sourceRootDirectory
     *            Directory for Java files to format and replace
     * @throws FileNotFoundException
     *             If required files or directories do not exist
     * @throws IOException
     *             If the creation of a new file or directory fails
     */
    public static void formatImportsAllFiles(String sourceRootDirectory)
            throws FileNotFoundException, IOException {
        formatAllFiles(sourceRootDirectory, sourceRootDirectory, true, false);
    }

    /**
     * Formats all Java files discovered in the supplied directory.
     * 
     * @param sourceRootDirectory
     *            Directory for Java files to format
     * @param targetRootDirectory
     *            Output directory for
     * @param imports
     *            Change fully qualified classnames to imports in file
     * @param jalopy
     *            Run jalopy against file
     * @throws FileNotFoundException
     *             If required files or directories do not exist
     * @throws IOException
     *             If the creation of a new file or directory fails
     */
    public static void formatAllFiles(String sourceRootDirectory,
            String targetRootDirectory, boolean imports, boolean jalopy)
            throws FileNotFoundException, IOException {
        String message = "Formatting ";
        if (imports) {
            message += "imports";
            if (jalopy) {
                message += " and jalopy";
            }
        } else if (jalopy) {
            message += "jalopy";
        }
        message += ": ";
        LOG.info(message + " under " + targetRootDirectory);

        JavaImportBeautifier beautifier = new JavaImportBeautifier();
        FormatManager manager = new FormatManager();
        manager.setConvention(beautifier);
        beautifier.setOrganizeImports(imports);
        // There are problems with the Jalopy parser for the generated files:
        // Unexpected char '<'
        beautifier.setFormat(jalopy);

        List<File> sourceFiles = manager.getAllJavaFiles(sourceRootDirectory);

        // Now reformat and save the targeted files to the target directory
        int listSize = sourceFiles.size();
        for (int i = 0; i < listSize; i++) {
            File sourceFile = (File) sourceFiles.get(i);
            LOG.info(message + sourceFile.getName() + " under "
                    + sourceFile.getParent());

            CharacterSequence sequence = new CharacterSequence(FileHelper
                    .loadStringBuffer(sourceFile));
            String original = sequence.getString();

            try {
                beautifier.beautify(sequence);
                // Only overwrite the file if the beautifier changes it, or if a
                // different directory
                // Sometimes Jalopy throws an error and returns a zero length
                // file.
                if (sequence.length() > 1
                        && (!original.equals(sequence.getString()))) {
                    File targetFile = new File((manager.getTargetDirectory(
                            sourceFile, sourceRootDirectory,
                            targetRootDirectory)), sourceFile.getName());
                    // File targetDirectory = new
                    // File((manager.getTargetDirectory(
                    // sourceFile, targetRootDirectory)), sourceFile.getName());
                    FileOutputStream outputSource = new FileOutputStream(
                            targetFile);
                    outputSource.write(sequence.getString().getBytes());
                    outputSource.flush();
                    outputSource.close();
                }
            } catch (RuntimeException e) {
                LOG.error("Exception formatting file: " + sourceFile.getName(),
                        e);
                // System.out.println("Exception formatting file: " +
                // sourceFile.getName());
                // e.printStackTrace();
            }
        }
        LOG.info("Finished Formatting all files under " + sourceRootDirectory);
    }

    /**
     * Formats a single Java file.
     * 
     * @param sourceFile
     *            Java file to format
     * @param targetRootDirectory
     *            Output directory for
     * @param imports
     *            Change fully qualified classnames to imports in file
     * @param jalopy
     *            Run jalopy against file
     * @throws FileNotFoundException
     *             If required files or directories do not exist
     * @throws IOException
     *             If the creation of a new file or directory fails
     */
    public static void formatFile(File sourceFile, String targetRootDirectory,
            boolean imports, boolean jalopy) throws FileNotFoundException,
            IOException {
        JavaImportBeautifier beautifier = new JavaImportBeautifier();
        FormatManager manager = new FormatManager();
        manager.setConvention(beautifier);
        beautifier.setOrganizeImports(imports);
        // There are problems with the Jalopy parser for the generated files:
        // Unexpected char '<'
        beautifier.setFormat(jalopy);

        LOG.info("Formatting file: " + sourceFile.getName());

        CharacterSequence sequence = new CharacterSequence(FileHelper
                .loadStringBuffer(sourceFile));

        try {
            beautifier.beautify(sequence);
            // Only overwrite the file if the beautifier changes it, or if a
            // different directory
            // Sometimes Jalopy throws an error and returns a zero length file.
            if (sequence.length() > 1) {
                File targetFile = null;
                if (targetRootDirectory == null
                        || "".equals(targetRootDirectory)) {
                    targetFile = sourceFile;
                } else {
                    targetFile = new File(targetRootDirectory, sourceFile
                            .getName());
                    targetFile = new File((manager.getTargetDirectory(
                            sourceFile, sourceFile.getParent(),
                            targetRootDirectory)), sourceFile.getName());
                }
                // File targetDirectory = new File((manager.getTargetDirectory(
                // sourceFile, targetRootDirectory)), sourceFile.getName());
                FileOutputStream outputSource = new FileOutputStream(targetFile);
                outputSource.write(sequence.getString().getBytes());
                outputSource.flush();
                outputSource.close();
            }
        } catch (RuntimeException e) {
            LOG.error("Exception formatting file: " + sourceFile.getName(), e);
            // System.out.println("Exception formatting file: " +
            // sourceFile.getName());
            // e.printStackTrace();
        }
        LOG.info("Finished Formatting all files");
    }

    /**
     * Locates and sets Jalopy formatting convention file
     * 
     * @param beautifier
     *            Instance of the beautifier class, not yet set to with external
     *            Jalopy formatting conventions
     * @throws FileNotFoundException
     *             If the convention file cannot be located
     */
    private void setConvention(JavaImportBeautifier beautifier) {
        String fileName = this.conventionFilePath + this.slash
                + this.conventionFileName;
        File conventionFile = new File(fileName);

        if (conventionFile.exists()) {
            beautifier.setConventionFilePath(fileName);
        } else {
            this.conventionFilePath = "/default-convention.xml";
            beautifier.setConventionFilePath("/default-convention.xml");
        }
    }

    /**
     * Fetches a target directory, creating it if it doesn't exist
     * 
     * @param sourceFile
     *            Filename to create
     * @param sourceRootDirectory
     *            sourceFile directory
     * @param targetRootDirectory
     *            target directory root to create and return
     * @return the target output directory for the specified re-formatted source
     *         File
     */
    private File getTargetDirectory(File sourceFile,
            String sourceRootDirectory, String targetRootDirectory) {
        String path = sourceFile.getParent();
        // Find the part of the source file below the source directory, append
        // to target directory
        int index = path.indexOf(sourceRootDirectory);
        if (index == -1) {
            if (sourceRootDirectory.indexOf("/") > -1) {
                // Pathname translation - replace doesn't work with //
                sourceRootDirectory = replace(sourceRootDirectory, "/", "\\");
                index = path.indexOf(sourceRootDirectory);
            } else {
                sourceRootDirectory = replace(sourceRootDirectory, "\\", "/");
                index = path.indexOf(sourceRootDirectory);
            }
        }
        String sourcePath = path
                .substring(index + sourceRootDirectory.length());
        if (targetRootDirectory == null || targetRootDirectory.equals("")) {
            targetRootDirectory = sourceRootDirectory;
        }
        File targetPath = new File(targetRootDirectory + sourcePath);
        targetPath.mkdirs();

        return targetPath;
    }

    /**
     * The String / stringBuffer replace command does not work with // !
     * 
     * @param target
     * @param toReplace
     * @param replaceBy
     * @return String with replaced tokens
     */
    public static String replace(String target, String toReplace,
            String replaceBy) {
        // StringBuffer result = new StringBuffer(target);
        int i = 0;
        // int j = 0;
        int toRepLength = toReplace.length();
        // int lengthDecrease = toRepLength - replaceBy.length();
        while ((i = target.indexOf(toReplace, i)) != -1) {
            target = target.substring(0, i)
                    + replaceBy
                    + new StringBuffer(target.substring(i + toRepLength, target
                            .length()));
            // result.replace(i - j, toRepLength, replaceBy);
            // i += toRepLength;
            // j += lengthDecrease;
        }
        return target;
    }

    /**
     * Traverses to the end of a directory tree, starting at indicated source
     * directory, and collects all found Java source files.
     * 
     * @param rootDirName
     *            The full path name of root of the directory tree to search for
     *            Java source files
     * @return List of Java source file discovered in the indicated directory
     *         tree, returned as Files
     * @throws FileNotFoundException
     *             if the root directory does not exist
     */
    private List<File> getAllJavaFiles(String rootDirName)
            throws FileNotFoundException {
        List<File> javaFiles = new ArrayList<File>();
        List<File> sourceDirs = new ArrayList<File>();
        File rootDir = asDirectory(rootDirName);

        // Traverse the directory tree from the starting source directory,
        // collect all the subdirectories
        List<File> subDirs = new ArrayList<File>();
        subDirs.add(rootDir);
        int listSize = 0;
        while (subDirs.size() > 0) {
            List<File> tempDirs = new ArrayList<File>();
            listSize = subDirs.size();
            for (int i = 0; i < listSize; i++) {
                File subDir = (File) subDirs.get(i);
                sourceDirs.add(subDir);
                tempDirs.addAll(getDirectories(subDir));
            }

            subDirs = tempDirs;
        }
        listSize = sourceDirs.size();
        for (int i = 0; i < listSize; i++) {
            File sourceDir = (File) sourceDirs.get(i);
            javaFiles.addAll(getJavaFiles(sourceDir));
        }

        return javaFiles;
    }

    /**
     * Collects the Java files in the indicated directory
     * 
     * @param dir
     *            The directory to search for Java source files
     * @return List of discovered Java source files as 'File' instances
     */
    private List<File> getJavaFiles(File dir) {
        List<File> files = new ArrayList<File>();

        // Capture the Java source in the source directory
        JavaSourceFilter javaFilter = new JavaSourceFilter();

        String[] fileNames = dir.list(javaFilter);

        int listSize = fileNames.length;

        for (int i = 0; i < listSize; i++) {
            String fileName = fileNames[i];

            File file = new File(dir.getAbsolutePath(), fileName);
            files.add(file);
        }

        return files;
    }

    /**
     * Collects the subdirectories for the indicated directory
     * 
     * @param dir
     *            The directory to find subdirectories for
     * @return List of discovered subdirectories as 'File' instances
     */
    private List<File> getDirectories(File dir) {
        List<File> dirs = new ArrayList<File>();

        // Capture the subdirectories the source directory
        DirectoryFilter dirFilter = new DirectoryFilter();

        String[] dirNameArray = dir.list(dirFilter);

        int listSize = dirNameArray.length;

        for (int i = 0; i < listSize; i++) {
            String dirName = dirNameArray[i];
            File subDir = new File(dir.getAbsolutePath(), dirName);
            dirs.add(subDir);
        }

        return dirs;
    }

    /**
     * Verifies the incoming path name exists and is a directory; if not throws
     * exception
     * 
     * @param dirPath
     *            Path to check
     * @return The directory specified in the incoming text as a File instance
     * @throws FileNotFoundException
     *             if the incoming path name does not exist or is not a
     *             directory
     */
    private File asDirectory(String dirPath) throws FileNotFoundException {
        File dir = new File(dirPath);

        if (!dir.isDirectory()) {
            StringBuffer msg = new StringBuffer(300);
            msg.append("Path ");
            msg.append(dirPath);
            msg.append(" does not exist or is not a directory.");
            throw new FileNotFoundException(msg.toString());
        }

        return new File(dirPath);
    }

    /**
     * This Filter identifies Java source code files
     */
    private class JavaSourceFilter implements FilenameFilter {
        /**
         * Checks for files ending with '.java', identifying them as Java source
         * 
         * @param dir
         *            the path name of the file to be filtered
         * @param name
         *            the name of the file (or last element of a directory path)
         *            of the file to be filtered
         * @return True If the file is a Java source file (has '.java'
         *         extension)
         */
        public boolean accept(File dir, String name) {
            return name.endsWith(".java");
        }
    }

    /**
     * This Filter identifies the directories with a list of files
     */
    private class DirectoryFilter implements FilenameFilter {
        /**
         * Filters incoming file to see if its a directory
         * 
         * @param dir
         *            Absolute path of the file being filtered
         * @param name
         *            he name of the file (or last element of a directory path)
         *            of the file to be filtered
         * @return True is a File is a directory
         */
        public boolean accept(File dir, String name) {
            File temp = new File(dir.getAbsoluteFile(), name);

            return temp.isDirectory();
        }
    }
}
